package com.wordpress.music2dot0.server;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.net.Socket;
import java.net.SocketException;
import java.util.Scanner;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Represents a single client
 * 
 * @author raf
 */
public class Connection implements Runnable {
	/**
	 * Convert a JAXB-annotated object to XML
	 * 
	 * @param o
	 *            a marshallable object
	 * @return the marshalled document (in XML)
	 */
	public static Document marshal(Object o) {
		try {
			final JAXBContext jaxbContext = JAXBContext.newInstance(o
					.getClass());
			final Marshaller marshaller = jaxbContext.createMarshaller();
			final DocumentBuilderFactory dbf = DocumentBuilderFactory
					.newInstance();
			final DocumentBuilder db = dbf.newDocumentBuilder();
			final Document doc = db.newDocument();
			marshaller.marshal(o, doc);
			return doc;
		} catch (final JAXBException e) {
			e.printStackTrace();
		} catch (final ParserConfigurationException e) {
			e.printStackTrace();
		}
		return null;
	}

	/**
	 * The last part of an incoming message
	 */
	private final String ENDTAG = "</data>";

	/**
	 * The group to which this client is currently joined
	 */
	private GroupServer group;

	/**
	 * The client socket of the connection
	 */
	private final Socket socket;

	/**
	 * The name of the client
	 */
	@XmlElement
	private String name;

	Connection() {
		this.socket = null;
	}

	/**
	 * Initializes a Connection with an existing socket
	 * 
	 * @param socket
	 *            a connected socket to a single client
	 */
	public Connection(Socket socket) {
		this.socket = socket;
	}

	/**
	 * Retrieves the name of the client
	 * 
	 * @return the current name
	 */
	public String getName() {
		return this.name;
	}

	/**
	 * Reads and interprets input from the client
	 * 
	 * @see java.lang.Runnable#run()
	 */
	@Override
	public void run() {
		try {
			final Scanner input = new Scanner(new BufferedReader(
					new InputStreamReader(this.socket.getInputStream())));
			input.useDelimiter(this.ENDTAG);
			final DocumentBuilder documentBuilder = DocumentBuilderFactory
					.newInstance().newDocumentBuilder();
			while (input.hasNext()) {
				// wait here for input
				final String inString = input.next().concat(this.ENDTAG);
				LoginServer.log.info("Message from "
						+ this.socket.getInetAddress() + ": " + inString);

				try {
					final Document message = documentBuilder
							.parse(new InputSource(new StringReader(inString)));
					final Node contentNode = message.getDocumentElement()
							.getFirstChild();
					final String nodeName = contentNode.getNodeName();
					if (nodeName.equals("join")) {
						if (this.group != null) {
							this.group.removeConnection(this);
						}
						this.group = LoginServer.getGroups().get(
								contentNode.getTextContent());
						this.group.addConnection(this);
						sendObject(this.group);
					} else if (nodeName.equals("login")) {
						GroupServerList groups = LoginServer.getGroups();
						final NamedNodeMap attributes = contentNode
								.getAttributes();
						if (attributes != null) {
							final Node xNode = attributes.getNamedItem("x");
							final Node yNode = attributes.getNamedItem("y");
							if (xNode != null && yNode != null) {
								final double x = Double.parseDouble(xNode
										.getNodeValue());
								final double y = Double.parseDouble(yNode
										.getNodeValue());
								groups = groups.near(x, y);
							}
						}
						this.name = contentNode.getTextContent();
						sendObject(groups);
					} else if (this.group != null) {
						if (nodeName.equals("vote")) {
							this.group.vote(contentNode.getTextContent());
							// } else if (nodeName.equals("leave")) {
							// this.group.removeConnection(this);
							// this.group = null;
						}
					}
				} catch (final SAXException e) {
					e.printStackTrace();
				} catch (final IOException e) {
					e.printStackTrace();
				}
			}
		} catch (final ParserConfigurationException e) {
			LoginServer.log.throwing("Connection", "run", e);
			e.printStackTrace();
		} catch (final IOException e) {
			LoginServer.log.throwing("Connection", "run", e);
			e.printStackTrace();
		}
	}

	/**
	 * Send a document to the connected client
	 * 
	 * @param document
	 *            the DOM-document to send
	 * @throws IOException
	 *             if the client is not connected
	 */
	public void send(Document document) throws IOException {
		final TransformerFactory tf = TransformerFactory.newInstance();
		try {
			final Transformer serializer = tf.newTransformer();
			final DOMSource source = new DOMSource(document);
			final StreamResult result = new StreamResult(this.socket
					.getOutputStream());
			serializer.transform(source, result);
		} catch (final SocketException e) {
			LoginServer.log.throwing("Connection", "send", e);
			// trust the GroupServer to throw us away
		} catch (final TransformerConfigurationException e) {
			LoginServer.log.throwing("Connection", "send", e);
			e.printStackTrace();
		} catch (final TransformerException e) {
			LoginServer.log.throwing("Connection", "send", e);
			LoginServer.log.info("Client disconnected.");
			this.socket.close();
		}
	}

	/**
	 * Sends a marshallable object to the client in XML
	 * 
	 * @param o
	 *            the annotated object to send
	 * @throws IOException
	 *             when the object can't be sent
	 */
	public void sendObject(Object o) throws IOException {
		send(Connection.marshal(o));
	}
}
